/*
 * Driver for the ZL380xx codec
 *
 * Copyright (c) 2018, Microsemi Corporation
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#define ZL380XX_INCLUDE_DEVICE_SUPPORT
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/pm.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/version.h>
#include <linux/err.h>
#include <linux/errno.h>

#include <linux/i2c.h>
#ifdef ZL380XX_INCLUDE_DEVICE_SUPPORT
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#endif

#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/initval.h>
#include <sound/soc.h>
#include <sound/soc-dapm.h>
#include <sound/pcm_params.h>
#include <sound/tlv.h>

#if VPROC_CODEC_MIXER_ENABLE
#include "typedefs.h"
#include "ssl.h"
#include "chip.h"
#include "hbi.h"
#endif

/* driver private data */
struct zl380tw {
    int sysclk;
    struct i2c_client   *i2c;
#if VPROC_CODEC_MIXER_ENABLE
    hbi_handle_t handle;
#endif    
} *zl380tw_priv;

/*--------------------------------------------------------------------
 *    ALSA  SOC CODEC driver
 *--------------------------------------------------------------------*/
#if VPROC_CODEC_MIXER_ENABLE
static int deviceId=0;

module_param(deviceId, uint, S_IRUGO);
MODULE_PARM_DESC(deviceId, "device Id (a value from 0 to VPROC_MAX_NUM_DEVS-1");

static int zl380tw_control_write(struct snd_kcontrol *kcontrol,
                                struct snd_ctl_elem_value *ucontrol);

static int zl380tw_control_read(struct snd_kcontrol *kcontrol,
                                struct snd_ctl_elem_value *ucontrol);
#endif  

/*Formatting for the Audio*/
#define zl380tw_DAI_RATES            (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000)
#define zl380tw_DAI_FORMATS          (SNDRV_PCM_FMTBIT_S16_LE)
#define zl380tw_DAI_CHANNEL_MIN      1
#define zl380tw_DAI_CHANNEL_MAX      2

static int zl380tw_hw_params(struct snd_pcm_substream *substream,
                            struct snd_pcm_hw_params *params,
                            struct snd_soc_dai *dai)
{

    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    struct snd_soc_codec *codec = rtd->codec;
    struct zl380tw *zl380tw = snd_soc_codec_get_drvdata(codec);
    int sample_rate = params_rate(params), bits_per_frame = 0;

	if (sample_rate) {
		bits_per_frame = zl380tw->sysclk / sample_rate;   
        dev_info(codec->dev, "TDM clk = %d, bits_per_frame = %d, sample_rate = %d", zl380tw->sysclk, bits_per_frame, sample_rate);
    }
    return 0;
}

static int zl380tw_set_dai_fmt(struct snd_soc_dai *codec_dai,
                              unsigned int fmt)
{
    /*the dai fmt is not needed-this is configured in the config record
     * our codec has the ability to auto-detect this info from the controller*/
    return 0;
}


static int zl380tw_set_dai_sysclk(struct snd_soc_dai *codec_dai, int clk_id,
                                    unsigned int freq, int dir)
{
    struct snd_soc_codec *codec = codec_dai->codec;
    struct zl380tw *zl380tw = snd_soc_codec_get_drvdata(codec);
    zl380tw->sysclk = freq;
    return 0;
}

static const struct snd_soc_dai_ops zl380tw_dai_ops = {
        .set_fmt        = zl380tw_set_dai_fmt,
        .set_sysclk     = zl380tw_set_dai_sysclk,
        .hw_params   	= zl380tw_hw_params,

};

static struct snd_soc_dai_driver zl380tw_dai = {
    .name = "zl380xx-dai",
    .playback = {
        .stream_name = "Playback",
        .channels_min = zl380tw_DAI_CHANNEL_MIN,
        .channels_max = zl380tw_DAI_CHANNEL_MAX,
        .rates = zl380tw_DAI_RATES,
        .formats = zl380tw_DAI_FORMATS,
    },
    .capture = {
        .stream_name = "Capture",
        .channels_min = zl380tw_DAI_CHANNEL_MIN,
        .channels_max = zl380tw_DAI_CHANNEL_MAX,
        .rates = zl380tw_DAI_RATES,
        .formats = zl380tw_DAI_FORMATS,
    },
    .ops = &zl380tw_dai_ops,
};
EXPORT_SYMBOL(zl380tw_dai);

#if VPROC_CODEC_MIXER_ENABLE
static const struct snd_kcontrol_new zl380tw_snd_controls[] = {
    SOC_SINGLE_EXT("DAC1 GAIN INA", ZL380xx_CP_DAC1_GAIN_REG, 0, 0x6, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("DAC2 GAIN INA", ZL380xx_CP_DAC2_GAIN_REG, 0, 0x6, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("DAC1 GAIN INB", ZL380xx_CP_DAC1_GAIN_REG, 8, 0x6, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("DAC2 GAIN INB", ZL380xx_CP_DAC2_GAIN_REG, 8, 0x6, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("MUTE SPEAKER ROUT", ZL380xx_AEC_CTRL0_REG, 7, 1, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("MUTE MIC SOUT", ZL380xx_AEC_CTRL0_REG, 8, 1, 0,
                    zl380tw_control_read, zl380tw_control_write),
    SOC_SINGLE_EXT("AEC MIC GAIN", ZL380xx_DIG_MIC_GAIN_REG, 0, 0x7, 0,
                    zl380tw_control_read, zl380tw_control_write),
	SOC_SINGLE_EXT("AEC ROUT GAIN", ZL380xx_ROUT_GAIN_CTRL_REG, 0, 0x78, 0,
		            zl380tw_control_read, zl380tw_control_write),
	SOC_SINGLE_EXT("AEC ROUT GAIN EXT", ZL380xx_ROUT_GAIN_CTRL_REG, 7, 0x7, 0,
		        	zl380tw_control_read, zl380tw_control_write),
	SOC_SINGLE_EXT("AEC SOUT GAIN", ZL380xx_SOUT_GAIN_CTRL_REG, 8, 0xf, 0,
		        	zl380tw_control_read, zl380tw_control_write),

};

static int zl380tw_control_read(struct snd_kcontrol *kcontrol,
                                struct snd_ctl_elem_value *ucontrol)
{
    struct soc_mixer_control *mc = (struct soc_mixer_control *)kcontrol->private_value;

    unsigned int reg = mc->reg;
    unsigned int shift = mc->shift;
    unsigned int mask = mc->max;
    unsigned int invert = mc->invert;
    unsigned char buf[2];
    hbi_status_t status =HBI_STATUS_SUCCESS;
    unsigned int val=0;

    status = HBI_read(zl380tw_priv->handle, reg, buf,2);
    val=buf[0];
    val=(val << 8)|buf[1];
    
    ucontrol->value.integer.value[0] = ((val >> shift) & mask);

    if (invert)
        ucontrol->value.integer.value[0] = mask - ucontrol->value.integer.value[0];

    return 0;
}


static int zl380tw_control_write(struct snd_kcontrol *kcontrol,
                                struct snd_ctl_elem_value *ucontrol)
{

    struct soc_mixer_control *mc = (struct soc_mixer_control *)kcontrol->private_value;

    reg_addr_t reg = mc->reg;
    unsigned int shift = mc->shift;
    unsigned int mask = mc->max;
    unsigned int invert = mc->invert;
    unsigned int val = (ucontrol->value.integer.value[0] & mask);
    unsigned int valt = 0;
    user_buffer_t buf[2];
    hbi_status_t status;

    if (invert)
        val = mask - val;

    status = HBI_read(zl380tw_priv->handle, reg, buf,2);
    if (status != HBI_STATUS_SUCCESS){
        return -EIO;
    }

    valt=buf[0];
    valt=(valt << 8)|buf[1];

    if (((valt >> shift) & mask) == val) {
        return 0;
    }


    valt &= ~(mask << shift);
    valt |= val << shift;

    buf[0]=valt>>8;
    buf[1]=valt&0xFF;

    status = HBI_write(zl380tw_priv->handle, reg,buf,2);

    if (status != HBI_STATUS_SUCCESS){
        return -EIO;
    }
    return 0;
}


int zl380tw_add_controls(struct snd_soc_codec *codec)
{
#if (LINUX_VERSION_CODE < KERNEL_VERSION(3,0,0))
    return snd_soc_add_controls(codec, zl380tw_snd_controls,
                                ARRAY_SIZE(zl380tw_snd_controls));
#else
    return snd_soc_add_codec_controls(codec, zl380tw_snd_controls,
                                ARRAY_SIZE(zl380tw_snd_controls));
#endif
}

static int zl380tw_codec_probe(struct snd_soc_codec *codec)
{
    hbi_status_t status;
    hbi_dev_cfg_t cfg;
    printk(KERN_INFO"Probing zl380tw SoC CODEC driver\n");
    
    status=HBI_init(NULL);
    if(status != HBI_STATUS_SUCCESS)
    {
        printk(KERN_ERR"Error in HBI_init()\n");
        return -1;
    }

    cfg.deviceId = deviceId;
    cfg.pDevName=NULL;

    status=HBI_open(&(zl380tw_priv->handle),&cfg);
    if(status != HBI_STATUS_SUCCESS)
    {
        printk(KERN_ERR"Error in HBI_open()\n");
        HBI_term();
        return -1;
    }

    if(zl380tw_add_controls(codec) < 0)
    {
        return -1;
    }


    return 0;
}

static int zl380tw_codec_remove(struct snd_soc_codec *codec)
{
    hbi_status_t status;
    
    status=HBI_close(zl380tw_priv->handle);
    
    return 0;
}
#endif

static struct snd_soc_codec_driver soc_codec_dev_zl380tw = {
#if VPROC_CODEC_MIXER_ENABLE 
    .probe =    zl380tw_codec_probe,
    .remove =   zl380tw_codec_remove,
#endif
};
EXPORT_SYMBOL(soc_codec_dev_zl380tw);

/*--------------------------------------------------------------------
 *    ALSA  SOC CODEC driver  - END
 *--------------------------------------------------------------------*/

#ifdef ZL380XX_INCLUDE_DEVICE_SUPPORT
/*IMPORTANT note: Change this controller string maching accordingly per your *.dts or *dtsi compatible definition file*/
static struct of_device_id zl380tw_of_match[] = {
    { .compatible = "microsemi,zl380snd0"},
    {},
};
MODULE_DEVICE_TABLE(of, zl380tw_of_match);
#endif

static int zl380tw_probe(struct i2c_client *i2c,
			    const struct i2c_device_id *id)
{

    int err = 0;
    
    /* Allocate driver data */
    zl380tw_priv = kzalloc(sizeof(*zl380tw_priv), GFP_KERNEL);
    if (zl380tw_priv == NULL)
        return -ENOMEM;

	i2c_set_clientdata(i2c, zl380tw_priv);
	zl380tw_priv->i2c = i2c;
    printk(KERN_INFO"probing zl380tw device\n");

    err = snd_soc_register_codec(&i2c->dev, &soc_codec_dev_zl380tw, &zl380tw_dai, 1);
    if(err < 0) {
        kfree(zl380tw_priv);
        zl380tw_priv=NULL;
        printk(KERN_ERR"zl380tw device not created!!!\n");
        return err;
    }

    printk(KERN_INFO"zl380tw codec device created...\n");
    return err;
}

static int zl380tw_remove(struct i2c_client *client)
{
    snd_soc_unregister_codec(&client->dev);
    kfree(i2c_get_clientdata(client));
    return 0;
}

static struct i2c_device_id zl380tw_id_table[] = {
    {"zl380snd0", 0 },
    {}
 };

MODULE_DEVICE_TABLE(i2c, zl380tw_id_table);

static struct i2c_driver zl380tw_codec_driver = {
	.driver = {
		.name = "zl380snd0",
		.owner = THIS_MODULE,
#ifdef ZL380XX_INCLUDE_DEVICE_SUPPORT		
		.of_match_table = zl380tw_of_match,
#endif
	},
    .probe      = zl380tw_probe,
    .remove     = zl380tw_remove,
	.id_table	= zl380tw_id_table,
};

static int bind_driver_to_master(void)
{
    /*Fake I2C device registration*/
#ifndef ZL380XX_INCLUDE_DEVICE_SUPPORT
    #define MICROSEMI_I2C_ADDR 0x52 
    struct i2c_board_info i2c_device_info = {
    	I2C_BOARD_INFO("zl380snd0", MICROSEMI_I2C_ADDR),
    	.platform_data	= NULL,
    };
	struct i2c_adapter *adapter = NULL;
        struct i2c_client *client = NULL;
     printk(KERN_INFO"binding zl380tw device...\n");
	adapter = i2c_get_adapter(0);
	if (!adapter)
		return -ENODEV;
	client = i2c_new_device(adapter, &i2c_device_info);
	if (!client)
		return -ENODEV;
	i2c_get_clientdata(client);

	i2c_put_adapter(adapter);
	printk(KERN_INFO"binding completed for zl380tw device sucessfully...\n");
#endif
	return 0;
}

static int __init zl380tw_modinit(void)
{
	int ret;
	ret = i2c_add_driver(&zl380tw_codec_driver);
	if (ret != 0)
		pr_err("Failed to register driver: %d\n", ret);
     
	return bind_driver_to_master();
}
module_init(zl380tw_modinit);

static void __exit zl380tw_exit(void)
{
	i2c_del_driver(&zl380tw_codec_driver);
}
module_exit(zl380tw_exit);

MODULE_AUTHOR("Jean Bony <jean.bony@microsemi.com>");
MODULE_DESCRIPTION(" Microsemi Timberwolf /alsa codec driver");
MODULE_LICENSE("GPL");
